<template>
    <div class="flex gap-4">
        <a-upload
            v-if="props.maxCount !== displayList.length"
            class="image-uploader"
            v-model:file-list="fileList"
            name="file"
            v-bind="uploadProps"
            :custom-request="handleUpload"
            :before-upload="beforeUpload"
            @change="handleChange"
        >
            <div>
                <loading-outlined v-if="loading"></loading-outlined>
                <div v-else>
                    <plus-outlined />
                    <div class="ant-upload-text">{{ buttonText }}</div>
                </div>
            </div>
        </a-upload>
        <!-- 图片已上传列表 -->
        <upload-image-preview-list :image-list="displayList" @on-preview="handlePreview" @on-remove="handleRemove" />
        <!-- 图片灯箱预览 -->
        <Lightbox
            :image-list="displayList"
            :visible="lightboxVisible"
            :initial-index="currentPreviewIndex"
            @update:visible="handleLightboxChange"
        />
    </div>
</template>
<script lang="ts" setup>
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons-vue";
import type { UploadChangeParam, UploadProps } from "ant-design-vue";
import { message } from "ant-design-vue";
import { ref } from "vue";

// 统一父子组件传输的文件URL格式，为完整的URL，因为外部肯定是直接传完整URL进行回显。内部也不要使用接口响应对象。
interface Props {
    // a-upload 组件属性
    showUploadList?: boolean; // 是否显示文件列表
    maxCount?: number; // 最大上传数量
    accept?: string; // 允许的文件类型 详见 input accept Attribute（https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept）
    multiple?: boolean; // 文件选择器中选择文件时是否支持多选，默认值为false。
    disabled?: boolean; // 是否禁用
    listType?: "text" | "picture" | "picture-card"; // 文件列表样式

    // 自定义props
    modelValue?: string | string[]; // 文件路径数组（外部传入回显，以及上传后返回）
    maxSize?: number; // 单个文件最大大小(MB)
    buttonText?: string; // 按钮文字
}

const props = withDefaults(defineProps<Props>(), {
    showUploadList: false,
    maxCount: 1,
    accept: ".png,.jpg,.jpeg,.gif,.webp",
    multiple: false,
    disabled: false,
    listType: "picture-card",

    maxSize: 10, // 10MB
    buttonText: "选择文件"
});

// props 中可直接应用到a-upload组件上并生效的属性
const uploadProps = computed<UploadProps>(() => ({
    showUploadList: props.showUploadList,
    multiple: props.multiple,
    maxCount: props.maxCount,
    accept: props.accept,
    disabled: props.disabled,
    listType: props.listType

    // 可以在这里添加其他需要的配置
    // 如 action, headers, data 等
}));

const emit = defineEmits<{
    "update:modelValue": [string | string[]];
}>();

const host = import.meta.env.VITE_API_BASE_URL;

const fileList = ref([]); // 选择的文件列表。受maxCount属性影响，如果maxCount为2，文件选择器选多了文件，也只会保留两个文件。
const loading = ref<boolean>(false);
// 图片灯箱
const lightboxVisible = ref(false);
const currentPreviewIndex = ref(0);

// 分离管理：外部回显 + 内部上传结果
const echoUrls = ref<string[]>([]); // 外部传入的回显URL
const uploadedUrls = ref<string[]>([]); // 本次上传的新URL

// 显示列表 = 回显 + 新上传
const displayList = computed(() => {
    // 因为新上传后的URL被push到了uploadedUrls，同时又emit更新了modelValue，
    // v-model又将该URL传了回来，导致回显的echoUrls也保存了一份
    // 因此需要去重
    return [...new Set([...echoUrls.value, ...uploadedUrls.value])];
});

// 向父组件输出 = 回显 + 新上传。
const outputValue = computed(() => {
    const allUrls = [...new Set([...echoUrls.value, ...uploadedUrls.value])];
    // 向父组件提供URL数据，如果上传数量为1，则返回字符串，否则返回数组
    return allUrls.length === 1 ? allUrls[0] : allUrls;
});

// 监听外部值变化（回显数据）
watch(
    () => props.modelValue,
    newValue => {
        if (!newValue) {
            echoUrls.value = [];
        } else {
            echoUrls.value = Array.isArray(newValue) ? [...newValue] : [newValue];
        }
    },
    { immediate: true, deep: true }
);

const allowImageMimeTypes = computed(() => {
    const defaultAllowTypes = ["image/jpeg", "image/png", "image/gif", "image/jpg"];
    const accept2MimeTypes = props.accept
        .trim()
        .split(",")
        .map(ext => "image/" + ext.slice(1));
    return [...defaultAllowTypes, ...accept2MimeTypes];
});
function getBase64(img: Blob, callback: (base64Url: string) => void) {
    const reader = new FileReader();
    reader.addEventListener("load", () => callback(reader.result as string));
    reader.readAsDataURL(img);
}

/**
 * a-upload 组件自带上传逻辑下的文件上传状态处理函数。上传中、完成、失败都会调用这个函数。
 * 注意：如果自定义了上传逻辑，则此函数无效。文件状态始终为 uploading。
 * @param info
 * file 当前操作的文件对象。
 * fileList 当前的文件列表。
 * event 上传中的服务端响应内容，包含了上传进度等信息，高级浏览器支持。
 */
const handleChange = (info: UploadChangeParam) => {
    if (info.file.status === "uploading") {
        loading.value = true;
        return;
    }
    if (info.file.status === "done") {
        // Get this url from response in real world.
        getBase64(info.file.originFileObj!, (base64Url: string) => {
            // imageUrl.value = base64Url;
            loading.value = false;
        });
    }
    if (info.file.status === "error") {
        loading.value = false;
        message.error("upload error");
    }
};
let curSeletedFileCount = 0;
// 自定义上传请求，每选择一个文件，就会执行一次。注意：不受maxCount属性限制。
const handleUpload: UploadProps["customRequest"] = async options => {
    curSeletedFileCount++;
    if (curSeletedFileCount > props.maxCount) {
        message.error(`最多只能选择${props.maxCount}个文件`);
        return;
    }
    loading.value = true;
    const file = options.file as File;
    const res = await Apis.Storage.upload({
        data: {
            dirId: 1,
            origin: "admin",
            hash: Date.now() + "",
            file
        }
    });
    // const res = {
    //     data: [
    //         {
    //             url: "http://localhost:7001/static/image/file-1763494053109_795847072-&image_jpeg&-image.jpg",
    //             path: "/static/image/file-1763494053109_795847072-&image_jpeg&-image.jpg"
    //         },
    //         {
    //             url: "http://localhost:7001/static/image/file-1763491047044_494683072-&image_jpeg&-image.jpg",
    //             path: "/static/image/file-1763491047044_494683072-&image_jpeg&-image.jpg"
    //         },
    //         {
    //             url: "http://localhost:7001/static/image/file-1763490261827_689142577-&image_jpeg&-image.jpg",
    //             path: "/static/image/file-1763490261827_689142577-&image_jpeg&-image.jpg"
    //         }
    //     ]
    // };
    res.data.forEach(info => {
        uploadedUrls.value.push(info.staticServeUrl);
    });

    loading.value = false;
    emit("update:modelValue", outputValue.value!);
};

// 上传之前的文件检查，每选择一个文件，就会执行一次。注意：不受maxCount属性限制。
const beforeUpload: UploadProps["beforeUpload"] = file => {
    const isLegalType = allowImageMimeTypes.value.includes(file.type);

    if (!isLegalType) {
        message.error("文件格式错误！");
        return false;
    }
    const isLegalSize = file.size / 1024 / 1024 < props.maxSize;
    if (!isLegalSize) {
        message.error(`文件大小不能超过 ${props.maxSize}MB！`);
        return false;
    }
    return isLegalType && isLegalSize;
};

const handlePreview = (urls: string[], index: number) => {
    currentPreviewIndex.value = index;
    lightboxVisible.value = true;
};
const handleRemove = async (url: string) => {
    // 选择的文件数量可能远远超过maxCount，因此删除时需要初始化为当前选择的合法的文件数量
    curSeletedFileCount = displayList.value.length;

    if (url.startsWith(host)) {
        await Apis.FileAdmin.removeBatchByUrl({
            data: {
                urls: [url]
            }
        });
    }

    curSeletedFileCount--;
    // 上传的图片URL在 echoUrls 和 uploadedUrls 都有一份，因此两个数组都要删除
    // 要不然只删一处，后续的去重也不会生效，因为压根不存在重复了，导致始终还有一份保留，删了好像没删
    const index1 = echoUrls.value.findIndex(item => item === url);
    if (index1 !== -1) echoUrls.value.splice(index1, 1);
    const index2 = uploadedUrls.value.findIndex(item => item === url);
    if (index2 !== -1) uploadedUrls.value.splice(index2, 1);

    emit("update:modelValue", outputValue.value!);
};

const handleLightboxChange = (val: boolean) => {
    lightboxVisible.value = val;
};
</script>
<style scoped lang="scss">
.image-uploader {
    width: fit-content;
}
.image-uploader > .ant-upload {
    width: 128px;
    height: 128px;
}
.ant-upload-select-picture-card i {
    font-size: 32px;
    color: #999;
}

.ant-upload-select-picture-card .ant-upload-text {
    margin-top: 8px;
    color: #666;
}
</style>
